
package com.borqs.music.lyric;

import java.io.UnsupportedEncodingException;
import java.util.Hashtable;
import java.util.Vector;

/**
 * the class is used to manage the lyric of MP3 file.the lyric format is
 * [mm:ss.ms]lyric\r\n
 */
public class Lyric {
    private Vector<TimeTag> mTags; // lyric time tags

    private Hashtable<Integer, String> mLyricsMap; // lyric lines

    private int mCurrentIndex; // current lyric index in tags vector

    private String mCharset = "UTF-8";

    /*
     * public static void main(String[] args) throws Exception { String url =
     * "e:/media/mp3/xmn.lrc"; File file = new File(url); if (file.exists()) {
     * FileInputStream fis = new FileInputStream(file); InputStreamReader reader =
     * new InputStreamReader(fis, "UTF-8"); int ch = -1; StringBuffer sb = new
     * StringBuffer(); while ((ch = reader.read()) != -1) { sb.append((char)
     * ch); } System.out.println(sb.toString()); Lyric l = new
     * Lyric(sb.toString().getBytes("UTF-8")); l.seekByTime(1024 * 40);
     * System.out.println(l.getCurrentLine()); } }
     */

    public Lyric(byte[] data, String charset) {
        this.mCharset = charset;
        mTags = new Vector<TimeTag>();
        mLyricsMap = new Hashtable<Integer, String>();
        parse(data);
    }

    /**
     * Seek to the right lyric line with the given time.
     *
     * @param time -
     *                in ms
     */
    public void seekByTime(long time) {
        if (mTags == null || mTags.size() < 1) {
            return;
        }
        if (time <= 0) {
            mCurrentIndex = 0;
        } else if ((time >= mTags.elementAt(mCurrentIndex).time) && (mCurrentIndex + 1 < mTags.size())
                && time < mTags.elementAt(mCurrentIndex + 1).time) {
            // currentIndex is still the right one, so do nothing
        } else if ((mCurrentIndex + 2 < mTags.size()) && (time >= mTags.elementAt(mCurrentIndex + 1).time)
                && (time < mTags.elementAt(mCurrentIndex + 2).time)) {
            // optimize for the normal switch to the next line, this is the most
            // case.
            mCurrentIndex++;
        } else if ((mCurrentIndex + 2 == mTags.size()) && (time >= mTags.elementAt(mCurrentIndex + 1).time)) {
            // optimize for the normal switch to the last 2nd line
            mCurrentIndex++;
        } else if ((mCurrentIndex >= mTags.size() - 1) && (time >= mTags.elementAt(mCurrentIndex).time)) {
            // currentIndex is the last one, so do nothing
        } else {
            // binary search to find the right one
            int low = 0;
            int mid = 0;
            int hi = mTags.size() - 1;
            while (hi > low) {
                mid = (low + hi) >> 1;
                if (time > mTags.elementAt(mid).time) {
                    low = ++mid;
                } else {
                    hi = mid;
                }
            }

            // adjust index because binary search is intended to locate next
            // line
            if (time < mTags.elementAt(mid).time && mid > 0 && mid > mCurrentIndex)
                mid--;

            mCurrentIndex = mid;
        }
    }

    /**
     * Get current lyric line.
     *
     * @return
     */
    public String getCurrentLine() {
        return getLineByIndex(mCurrentIndex);
    }

    /**
     * Get current lyrics line index
     */
    public int getCurrentIndex() {
        return mCurrentIndex;
    }

    /**
     * Get previous lyric line.
     *
     * @return
     */
    public String getPrevious() {
        return getLineByIndex(mCurrentIndex - 1);
    }

    /**
     * Get next lyric line.
     */
    public String getNextLine() {
        return getLineByIndex(mCurrentIndex + 1);
    }

    /**
     * Get remaining lines of lyrics
     */
    public int getRemainLine() {
        return mTags.size() - mCurrentIndex;
    }

    /**
     * Get line count of lyrics
     */
    public int getLineCount() {
        return mTags.size();
    }

    /**
     * Get time of next lyric line or -1 if no more lyric line
     */
    public int getNextTime() {
        if (mCurrentIndex < mTags.size() - 1) {
            return mTags.elementAt(mCurrentIndex + 1).time;
        } else {
            return -1;
        }
    }

    /**
     * Get the lyric line specified by the index.
     *
     * @param index -
     *                lyric index in tags vector
     * @return The lyric line
     */
    public String getLineByIndex(int index) {
        if (index < 0 || index >= mTags.size()) {
            return null;
        }

        return mLyricsMap.get(mTags.elementAt(index).index);
    }

    /**
     * Get one valid lyric time tag.
     *
     * @param data -
     *                lyric data from lrc file
     * @param offset -
     *                offset in lyric data
     * @param len -
     *                lyric data length
     * @param index -
     *                lyric line index
     * @param lycOffset
     * @return TimeTag - a lyric time tag if found, otherwise null
     */
    private TimeTag getTimeTag(byte[] data, int offset, int len, int index, int lycOffset) {
        // lyric time tag format: [mm:ss.xxx], or [mm:ss] or [m:ss.xxx] or
        // others

        // first find ], and make a basic validation
        int rightBracket = LyricUtils.indexOf(data, (byte) ']', offset, len);
        if (rightBracket < 4 || rightBracket > 10) {
            return null;
        }
        int rightBracketIndex = offset + rightBracket;
        // parse minutes
        int time = 0;
        int colon = LyricUtils.indexOf(data, (byte) ':', offset, rightBracket);
        if (colon == 2) {
            if (!Character.isDigit(data[offset + 1])) {
                return null;
            }
            time = (data[offset + 1] - '0') * 60000;
        } else if (colon == 3) {
            if (!Character.isDigit(data[offset + 1]) || !Character.isDigit(data[offset + 2])) {
                return null;
            }
            time = ((data[offset + 1] - '0') * 10 + (data[offset + 2] - '0')) * 60000;
        } else {
            return null;
        }
        int colonIndex = offset + colon;
        // parse second
        int dot = LyricUtils.indexOf(data, (byte) '.', colonIndex, rightBracket);
        if (dot == -1) {
            if ((rightBracketIndex - colonIndex) == 2) {
                if (!Character.isDigit(data[colonIndex + 1])) {
                    return null;
                }
                time += (data[colonIndex + 1] - '0') * 1000;
            } else if ((rightBracketIndex - colonIndex) == 3) {
                if (!Character.isDigit(data[colonIndex + 1]) || !Character.isDigit(data[colonIndex + 2])) {
                    return null;
                }
                time += ((data[colonIndex + 1] - '0') * 10 + (data[colonIndex + 2] - '0')) * 1000;
            } else {
                return null;
            }
        } else {
            if (dot == 2) {
                if (!Character.isDigit(data[colonIndex + 1])) {
                    return null;
                }
                time += (data[colonIndex + 1] - '0') * 1000;
            } else if (dot == 3) {
                if (!Character.isDigit(data[colonIndex + 1]) || !Character.isDigit(data[colonIndex + 2])) {
                    return null;
                }
                time += ((data[colonIndex + 1] - '0') * 10 + (data[colonIndex + 2] - '0')) * 1000;
            } else {
                return null;
            }

            // parse millisecond
            int dotIndex = colonIndex + dot;
            for (int i = dotIndex + 1; i < rightBracket; i++) {
                if (!Character.isDigit(data[i])) {
                    return null;
                }
            }
            String s = LyricUtils.getUTF8String(data, dotIndex + 1, rightBracketIndex - dotIndex - 1);
            time += LyricUtils.getInt(10, s, 0);
        }

        return new TimeTag(time - lycOffset, index);
    }

    /**
     * Save a lyric time tag. Note that the time tags are sorted by time.
     *
     * @param tag
     */
    private void insertTimeTag(TimeTag tag) {
        // binary search to find the right slot to insert
        int low = 0;
        int mid = 0;
        int hi = mTags.size();
        while (hi > low) {
            mid = (low + hi) >> 1;
            if (tag.time > ((TimeTag) mTags.elementAt(mid)).time) {
                low = ++mid;
            } else {
                hi = mid;
            }
        }
        mTags.insertElementAt(tag, mid);
    }

    /**
     * Parse lyric data.
     *
     * @param data - byte array from lrc file
     */
    private void parse(byte[] data) {
        int len = data.length;
        int i = 0;
        int j = 0;
        int index = 0;
        int offset = 0;
        boolean gettingLyric = false;

        while (i < len) {
            // skip any leading characters before '['
            if (data[i] != '[' && !gettingLyric) {
                i++;
                j++;
            } else if (data[i] == '[' && !gettingLyric) {
                TimeTag tag = getTimeTag(data, i, len, index, offset);

                if (tag != null) {
                    gettingLyric = true;
                    // handle multiple time tags in one line
                    while (tag != null) {
                        insertTimeTag(tag);

                        // adjust i after ]
                        int bracket = LyricUtils.indexOf(data, (byte) ']', i, len);
                        i += bracket == -1 ? 1 : (bracket + 1);
                        j = i;
                        tag = getTimeTag(data, i, len, index, offset);
                    }
                } else { // unknow tag
                    // check offset tag
                    if (data[i + 1] == 'o' && data[i + 2] == 'f' && data[i + 3] == 'f' && data[i + 4] == 's'
                            && data[i + 5] == 'e' && data[i + 6] == 't' && data[i + 7] == ':') {
                        i += 8;
                        j = i;
                        while (i < len && data[i] != '[') {
                            if (data[i] == ']') {
                                String offsetStr = LyricUtils.getUTF8String(data, j, i - j).trim();
                                offset = LyricUtils.getInt(10, offsetStr, 0);
                                break;
                            }

                            i++;
                        }

                        j = i;
                    } else {
                        i++;
                        j++;
                    }
                }
            } else if (gettingLyric) {
                while (i < len) {
                    if (data[i] == '\r' || data[i] == '\n') {
                        break;
                    } else {
                        i++;
                    }
                }

                String lyric = null;
                try {
                    lyric = new String(data, j, i - j, mCharset == null ? "UTF-8" : mCharset);
                } catch (UnsupportedEncodingException e) {
                    lyric = "UnsupportedEncodingException:" + e.getMessage();
                }
                mLyricsMap.put(index, lyric);
                lyric = null;

                gettingLyric = false;
                index++;
                i++;
                j = i;
            } // end of "} else if (gettingLyric) {"
        } // end of "while (i < len) {"
    }

    public String toString() {
        return mTags.toString() + "\n" + mLyricsMap.toString();
    }

    /**
     * Private class for hold lyric time tag.
     *
     */
    private class TimeTag {
        private int time; // lyric time tag in ms

        private int index; // lyric index

        private TimeTag(int time, int index) {
            this.time = time;
            this.index = index;
        }

        public String toString() {
            return "[" + time + ":" + index + "]";
        }
    }

}
